<script lang="ts">
	import { onMount } from 'svelte';
	import { Story, Compiler } from 'inkjs/compiler/Compiler';
	import type { Choice } from 'inkjs/engine/Choice';

	const globLoaders = import.meta.glob('../stories/**/*.ink', { query: '?raw', import: 'default' });

	let story: Story;
	let text: string | null = null;
	let choices: Choice[] = [];
	let history: { text: string | null; state: string }[] = [];
	let activeKey: string | null = null;
	let fileMap: Record<string, string> = {};

	onMount(async () => {
		await loadAllInkAndCompile();
	});

	function normalize(fullPath: string): string {
		return fullPath.replace(/^\.\.\/stories\//, '');
	}

	async function loadAllInkAndCompile() {
		fileMap = {};
		for (const [path, loader] of Object.entries(globLoaders)) {
			const rel = normalize(path);
			const content = await (loader as () => Promise<string>)();
			fileMap[rel] = content;
		}
		const keys = Object.keys(fileMap).sort();
		let key: string | null =
			keys.find((k) => k.endsWith('/main.ink')) ??
			keys.find((k) => k === 'main.ink') ??
			keys[0] ??
			null;
		if (!key) {
			text = 'No .ink stories found in src/stories.';
			choices = [];
			return;
		}
		activeKey = key;
		compileActive();
	}

	function buildInlinedContent(entry: string, stack = new Set<string>()): string {
		if (stack.has(entry)) return '';
		stack.add(entry);
		let src = fileMap[entry] ?? '';
		// Replace INCLUDE lines robustly (supports LF/CRLF)
		src = src.replace(/(^|\r?\n)\s*INCLUDE\s+(.+?)\s*(?=\r?\n|$)/g, (_m, prefix, inc) => {
			inc = String(inc)
				.replace(/['\"<>]/g, '')
				.trim();
			const baseDir = entry.replace(/[^/]+$/, '');
			let candidate = baseDir + inc;
			if (!fileMap[candidate]) {
				if (fileMap[inc]) candidate = inc;
				else {
					const found = Object.keys(fileMap).find((k) => k.endsWith('/' + inc));
					if (found) candidate = found;
					else return prefix + '// (Missing INCLUDE ' + inc + ')\n';
				}
			}
			// Ensure a newline after included content so following root lines aren't concatenated.
			return prefix + buildInlinedContent(candidate, stack) + '\n';
		});
		return src;
	}

	function compileActive() {
		if (!activeKey) return;
		// Always inline includes starting from the entry to avoid filename resolution issues
		const inlined = buildInlinedContent(activeKey);
		try {
			let compiler = new Compiler(inlined);
			story = compiler.Compile();
		} catch (e: any) {
			text = 'Compilation error: ' + e;
			choices = [];
			return;
		}
		updateStory();
	}

	function updateStory() {
		text = '';
		while (story.canContinue) text += story.Continue();
		choices = story.currentChoices;
	}

	function makeChoice(choiceIndex: number) {
		history = [...history, { text, state: story.state.ToJson() }];
		story.ChooseChoiceIndex(choiceIndex);
		updateStory();
	}

	function undo() {
		if (history.length > 0) {
			const previous = history[history.length - 1];
			history = history.slice(0, -1);
			if (previous) {
				story.state.LoadJson(previous.state);
				text = previous.text;
				choices = story.currentChoices;
			}
		}
	}

	if (import.meta.hot) {
		import.meta.hot.accept(async () => {
			const prevState = story?.state?.ToJson?.();
			await loadAllInkAndCompile();
			if (prevState && story) {
				try {
					story.state.LoadJson(prevState);
				} catch {}
			}
			history = [];
			updateStory();
		});
	}
</script>

<div
	class="min-h-screen bg-gradient-to-br from-neutral-50 via-neutral-100 to-neutral-200 py-10 dark:from-neutral-900 dark:via-neutral-800 dark:to-neutral-700"
>
	<div class="mx-auto max-w-3xl px-6">
		<div
			class="rounded-xl border border-neutral-200/70 bg-white/80 shadow-lg backdrop-blur transition-colors dark:border-neutral-700 dark:bg-neutral-900/70"
		>
			<div class="prose max-w-none p-8 prose-neutral dark:prose-invert">{@html text}</div>
			<div class="flex flex-col gap-3 px-8 pb-8">
				{#each choices as choice}
					<button
						class="group relative overflow-hidden rounded-lg border border-neutral-300 bg-neutral-100/90 px-5 py-3 text-left text-sm font-medium text-neutral-800 shadow-sm transition-all hover:bg-neutral-200 hover:shadow-md focus-visible:ring-2 focus-visible:ring-neutral-500 focus-visible:outline-none dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
						on:click={() => makeChoice(choice.index)}
					>
						<span
							class="absolute inset-0 h-full w-full scale-0 bg-neutral-300/40 opacity-0 transition-all duration-300 group-hover:scale-100 group-hover:opacity-100 dark:bg-neutral-700/40"
						></span>
						<span class="relative">{choice.text}</span>
					</button>
				{/each}
				{#if history.length > 0}
					<div class="flex justify-end border-t border-neutral-200 pt-4 dark:border-neutral-700">
						<button
							class="inline-flex items-center gap-2 rounded-md bg-neutral-800 px-4 py-2 text-sm font-semibold text-neutral-100 shadow-sm transition-colors hover:bg-neutral-700 focus-visible:ring-2 focus-visible:ring-neutral-500 focus-visible:outline-none dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
							on:click={undo}
						>
							<span aria-hidden="true">⟲</span> 回退
						</button>
					</div>
				{/if}
			</div>
		</div>
		<div class="mt-6 text-center text-xs text-neutral-500 dark:text-neutral-400">
			Ink story demo · Press 回退 to go back a choice.
		</div>
	</div>
</div>
